# 객체 프로퍼티

# 프로퍼티의 종류

  1. 데이터 프로퍼티 (data property)
  2. 접근자 프로퍼티 (accessor property)
    • 본질은 함수
    • 값을 획득(get) / 설정(set)
    • 외부 코드에서는 함수가 아닌 일반적인 프로퍼티처럼 보인다.

둘 중 한 종류에만 속할 수 있다.

Object.defineProperties({}, 'prop', {
    get() { // 접근자 프로퍼티의 설명자
        return 1;
    },
    value: 2    // 데이터 프로퍼티의 설명자
}) // error
1
2
3
4
5
6

# 프로퍼티 특성 설명자 Property Descriptor

ES5 이후, descriptor 값의 조회/수정이 가능해졌다.

# 프로퍼티 값과 플래그

객체 프로퍼티: 값(value) + 플래그(flag) 속성 세 가지

  • value 프로퍼티 값 정의

    • type: 자바스크립트에서 허용한 모든 값
    • Default: undefined
  • writable

    • true 값을 수정할 수 있다.
  • enumerable

    • true 반복문을 사용해 나열 가능
  • configurable

    • true 프로퍼티 삭제/플래그 수정 가능

프로퍼티 플래그의 기본값은 모두 true

# Object.getOwnPropertyDescriptor(obj, propertyName)

  • 특정 프로퍼티에 대한 정보를 모두 반환
  • obj 정보를 얻고자 하는 객체
  • propertyName 정보를 얻고자 하는 객체 내 프로퍼티
let user = {
  name: "John"
};

let descriptor = Object.getOwnPropertyDescriptor(user, 'name');

alert( JSON.stringify(descriptor, null, 2 ) );
/* property descriptor:
{
  "value": "John",
  "writable": true,
  "enumerable": true,
  "configurable": true
}
*/
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

# Object.defineProperty(obj, propertyName, descriptor)

  • 프로퍼티 플래그 변경

  • [obj, propertyName] = [설명자를 적용하고 싶은 객체, 객체 프로퍼티]

    • propertyName 이 없으면, 새로운 프로퍼티 생성
  • [descriptor] = [적용하고자 하는 프로퍼티 설명자]

  • 플래그 정보가 없으면 defaultfalse

let user = {};

Object.defineProperty(user, "name", {
  value: "John"
});

let descriptor = Object.getOwnPropertyDescriptor(user, 'name');

alert( JSON.stringify(descriptor, null, 2 ) );
/*
{
  "value": "John",
  "writable": false,
  "enumerable": false,
  "configurable": false
}
 */
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

# writable 플래그

let user = {
  name: "John"
};

Object.defineProperty(user, "name", {
  writable: false
});

user.name = "Pete"; // Error: Cannot assign to read only property 'name'
1
2
3
4
5
6
7
8
9

비엄격 모드

  • 읽기 전용 프로퍼티 값 수정 -> 에러 X
  • BUT, 값 변경 X
  • 플래그에서 정한 규칙을 위반 -> 에러 없이 그냥 무시
let user = { };

Object.defineProperty(user, "name", {
  value: "John",
  // defineProperty를 사용해 새로운 프로퍼티를 만들 땐, 어떤 플래그를 true로 할지 명시해주어야 합니다.
  enumerable: true,
  configurable: true
});

alert(user.name); // John
user.name = "Pete"; // Error
1
2
3
4
5
6
7
8
9
10
11

# enumerable 플래그

객체 내장 메서드 toString

  • non-enumerable
  • for...in, Object.keys(), Object.entries(), Object.values() 에서 순회하지 않음.
  • custom toString 을 추가하면 for..in 에서 나타남
let user = {
  name: "John",
  toString() {
    return this.name;
  }
};

//커스텀 toString은 for...in을 사용해 열거할 수 있습니다.
for (let key in user) alert(key); // name, toString

Object.defineProperty(user, "toString", {
  enumerable: false
});

// 이제 for...in을 사용해 toString을 열거할 수 없게 되었습니다.
for (let key in user) alert(key); // name

// 열거가 불가능한 프로퍼티는 Object.keys에도 배제됩니다.
alert(Object.keys(user)); // name
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

# configurable 플래그

false

  • 해당 프로퍼티는 제거/변경 X
  • configurable/enumerable 플래그 수정 X
  • writable
    • false -> true (X)
    • true -> false (O)
  • 접근자 프로퍼티 get/set 을 변경 X (새롭게 만드는 것은 가능)

# Object.defineProperties(obj, descriptors)

프로퍼티 여러개를 한 번에 정의할 수 있다.

Object.defineProperties(user, {
  name: { value: "John", writable: false },
  surname: { value: "Smith", writable: false },
  // ...
});
1
2
3
4
5

# Object.getOwnPropertyDescriptors

프로퍼티 설명자를 전부 한꺼번에 가져올 수 있다.

  • Object.defineProperties 와 함께 사용하면
    • 플래그, 심볼형 프로퍼티도 함께 복사
let clone = Object.defineProperties({}, Object.getOwnPropertyDescriptors(obj));
1

# 객체 수정을 막아주는 다양한 메서드

  • Object.preventExtensions(obj) 객체에 새로운 프로퍼티를 추가할 수 없게 합니다.
  • Object.seal(obj) 새로운 프로퍼티 추가나 기존 프로퍼티 삭제를 막아줍니다. 프로퍼티 전체에 configurable: false를 설정하는 것과 동일한 효과입니다.
  • Object.freeze(obj) 새로운 프로퍼티 추가나 기존 프로퍼티 삭제, 수정을 막아줍니다. 프로퍼티 전체에 configurable: false, writable: false를 설정하는 것과 동일한 효과입니다.

제약 사항을 확인하는 메서드

  • Object.isExtensible(obj) 새로운 프로퍼티를 추가하는 게 불가능한 경우 false를, 그렇지 않은 경우 true를 반환합니다.
  • Object.isSealed(obj) 프로퍼티 추가, 삭제가 불가능하고 모든 프로퍼티가 configurable: false이면 true를 반환합니다.
  • Object.isFrozen(obj) 프로퍼티 추가, 삭제, 변경이 불가능하고 모든 프로퍼티가 configurable: false, writable: false이면 true를 반환합니다.

# 접근자 프로퍼티 설명자 Accessor Descriptor

  • value, writable 은 무시된다.
  • get 인수가 없는 함수, 프로퍼티를 읽을 때 동작
  • set 인수가 하나인 함수, 프로퍼티에 값을 쓸 때 호출됨
  • enumerable
  • configurable
let obj = {
    get propName() {
        // getter, obj.propName 을 실행할 때 실행된다.
    },
    set propName(value) {
        // setter, obj.propName = value 를 실행할 때 실행되는 코드
    }
}
1
2
3
4
5
6
7
8

바깥 코드에선 접근자 프로퍼티를 일반 프로퍼티처럼 사용할 수 있다.

let user = {
    name: "John",
    surname: "Smith",
    get fullName() {
        return `${this.name} ${this.surname}`;
    }
};
alert(user.fullName);
1
2
3
4
5
6
7
8

프로퍼티에 getter 메서드만 있기 때문에 에러가 발생한다.

user.fullName = 'Test'; // Error
1

setter 추가

let user = {
    // ...
    set fullName(value) {
        [this.name, this.surname] = value.split(" ");
    }
};
user.fullName = "Alice Cooper";
console.log(user.name, user.surname);   // Alice Cooper
1
2
3
4
5
6
7
8

fullName 은 가상의 프로퍼티 이다. 읽고 쓸 순 있지만 실존하지 않는다.

# 자바 getters, setters 의 용도

🔗 🔗

  1. 유효성 검사
  2. Lazy Loading
  3. Read 와 Write 권한을 다르게 설정

# 유효성 검사

클래스 외부에서 클래스의 private 필드 를 get/set 할때 사용한다.

  • 클래스 필드를 실수로 조작하지 못하도록 유효성 검사를 수행 할 수있는 중심 위치.

# 캡슐화

# 동시성과 멀티스레딩 -> 불변객체 생성

  • 객체의 상태의 불변성을 위해 setter 가 없다.
  • 객체를 동시에 실행중인 다른 스레드로 전달하려면, race condition 및 기타 멀티스레딩의 부작용을 피하기 위해 스레드들을 동기화 해야 한다.
  • 불변 객체의 경우 객체 상태가 스레드에 의해 변경될까봐 걱정할 필요가 없다.
  • 불변 객체 예제
public class MyObject {
    // 모든 멤버 변수는 private - 캡슐화, final 
    private final int state;
    private final String str;
    // constructor(생성자) 는 변수를 정의 할 수있는 마지막 장소이다.
    public MyObject(int state, String str) {
        this.state = state;
        this.str = str;
    }	
    
    //Getters
    public int getState() {
        return this.state;		
    }
    
    public String getStr() {
        return this.str;
    }
    // 객체의 상태의 불변성을 위해 setter 가 없다.
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

# getter 획득자 메서드

보통, 계산된 값을 반환

# Syntax

  • 함수 이름
    • 자바스크립트 변수 이름 생성 규칙 (_$0-9, A-Z,a-z) 에 허용 되는 문자열.
  • 매개변수가 없다

# 선언

var log = ['test'];
var obj = {
  get latest () {
    if (log.length == 0) return undefined;
    return log[log.length - 1]
  }
}
1
2
3
4
5
6
7

프로퍼티 접근을 해도, 정의된 함수가 호출되고 값이 반환된다.

console.log (obj.latest); // "test"를 반환.
1

# 삭제

delete obj.latest
1

# defineProperty, 객체에 새 프로퍼티로 추가

const obj = { _name: 'DoonDoony' };
Object.defineProperty(obj, 'name', {
    get: function() {
        return `${this._name} the Cat!`;
    },
    enumerable: true,
    configurable: true,
});
console.log(obj.name); // "DoonDoony the Cat!"
1
2
3
4
5
6
7
8
9

# 계산된 프로퍼티 이름

const prefix = 'name';
const obj = { 
  get [prefix] () { 
    return 'Getter is called with prefix' 
  }
}
1
2
3
4
5
6

# Immutable 패턴

객체의 생명주기 동안 내부의 상태가 절대 변경되지 않도록 강제하는 방법

  • 객체의 프로퍼티 값 할당을 생성자를 통해서만 할 수 있다.
  • 필드에 접근하기 위해서는 Getter 메서드를 사용해야 한다.
const _list = new WeakMap();
const key = new Object();

class SomeClass {

    constructor(list) {
	_list.set(key, list);
    }

    // 필드 자체 값 대신에 복사본을 반환
    getList() {
        // 객체를 복제하는 clone()이라는 메서드가 있다고 가정
        return _list.get(key).clone();
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

# lazy evaluation

getter 프로퍼티에 접근하기 전까지는 값을 계산하지 않는다.

  • 값의 계산 비용이 큰 경우
    • RAM 이나 CPU 의 시간을 많이 소모할 때
    • worker thread 생성 (?)
    • 원격 파일 로드
  • 값이 당장 필요하지 않을때, 나중에 이용될 때, 절대로 이용되지 않을 때
  • 값이 다시 계산되어서는 안되는 경우.
  • 값이 여러차례 이용되지만, 절대 변경되지 않아 매번 다시 계산할 필요가 없을 때

각 케이스가 궁금하다 TODO

# getter 반환값의 캐싱

  • getter 은 첫 호출 이후에는 다시 계산하지 않고 이 캐시 값을 반환한다.

  • setter 에서 delete 를 하든 getter 에서 delete 를 하든, delete 를 호출하면 setter, getter 모두가 해제 된다.

const o = {
  _foo: '',
  set foo (val) {
    delete this.foo;
    this.foo = val;
  },
  get foo () {
    delete this.foo;
    return this.foo = 'something';
  }
};

o.foo = "test";
console.log(o.foo); // 'test' 출력
1
2
3
4
5
6
7
8
9
10
11
12
13
14
  • o.foo = "test" 실행 -> getter 인 foo 삭제
  • console.log(o.foo) 실행
    • get foo() {} 를 실행 X
    • o.foo 프로퍼티의 값을 반환하여 "test" 를 출력 O

🔗 o.foo = "test" 이후의 setter 가 활성화 되고, delete this.foo 는 setter 를 삭제하여 다음줄의 this.foo 는 객체의 프로퍼티에 값을 할당하게 됩니다. 그러나 만약에 delete this.foo 가 없다면, this.foo는 다시 setter 를 호출하게 되므로 throws RangeError maximum call를 유발합니다. 이것의 대안은 this._foo 처럼 프로퍼티 명을 private 형식으로 지정해 줄 수 있습니다.

var o = {
  set foo (val) {
    this._foo = val;
  },
    // ...
};
1
2
3
4
5
6

get foo() { delete this.foo } foo 프로퍼티를 삭제해도 결과는 test 을 출력한다. 왜 delete 를 적어준 것일까요?

  • 다시 o.foo 를 얻어 올때, getter 으로 값을 다시 계산하는 것을 없애기 위해서 입니다. 단순히 o의 프로퍼티인 foo 로서 값을 얻어 옵니다.
var o = {
  _foo: '',
  set foo (val) {
    this._foo = val;
  },
  get foo () {
    return this._foo = 'something';
  }
};
1
2
3
4
5
6
7
8
9
  • return this._foo = 'something'; 문자열을 반환하는 이유
    • 자바스크립트에서 할당문은 할당한 값을 반환 (const, let, var 키워드 제외)
      이 예제는 getter 와 setter 를 캐싱으로서 사용하지 않을 때 입니다.
o.foo = "test"; //test 
o.foo; // something
1
2

아무리 setter 를 해서 _foo 의 값을 변경 시켜도, o.foo 으로 getter 를 호출해도 항상 'something' 으로 됩니다. this._foo = 'something'에서 다시 setter 를 호출하기 때문입니다. 이 예제는 도대체 왜 만든건지 모르겠네요.

중요한 것은, 캐싱을 위해 의도적으로 getter 와 setter 를 사용할 수 있다는 점 같습니다.

# setter 설정자 메서드

const cat = {
  _name: undefined,
  set name(newName) {
    this._name = newName;
  },
  call() {
    console.log(this._name);
  },
};

cat.name = 'DoonDoony';
cat.call(); // 'DoonDoony'
1
2
3
4
5
6
7
8
9
10
11
12

# setter 에서 값을 할당하는 내부적 과정

# 1. 접근하는 프로퍼티 탐색

직속 프로퍼티

  1. 접근하는 프로퍼티가 Accessor Descriptor(Getter/Setter) 일 때

    • [Setter] 호출.
    • [Setter] 가 없고 [Getter]만 있을 때
      • 값 할당은 무시된다.
  2. 접근자 설명자가 아니고, writable: false 일 때

    • 조용히 실패
    • 엄격모드일 땐, Type Error 발생
  3. 1번 2번 모두 해당하지 않을때, 프로퍼티에 값을 세팅

  4. 접근하는 프로퍼티가 없을 때

    • 객체와 연결된 상위 [[Prototype]] 체인을 순회한다.

# 2. 모든 [[Prototype]] 체인에서 프로퍼티가 발견되지 않을 경우


  1. Object extensible
    • 직속 프로퍼티(Directly Present) 생성하고 값을 할당
  2. not Object extensible
    • Object.preventExtensions(), Object.isExtensible()[false] 일 때,
      'use strict';
      
      const parentCat = {
        name: 'DoonDoon',
        age: 10,
      };
      
      const childCat = Object.create(parentCat);
      Object.assign(childCat, { name: 'DoonDoony', age: 3, favorite: 'Red Ball' });
      
      1
      2
      3
      4
      5
      6
      7
      8
      9
      childCat 을 확장 불가능하게 만듭니다
      Object.preventExtensions(childCat);
      // childCat 이 확장 불가능한지 확인합니다
      console.log(Object.isExtensible(childCat)); // false
      
      1
      2
      3
    • TypeError 발생
      childCat.gender = 'male'; // TypeError!
      
      1
    • 프로퍼티의 삭제/수정 가능
      childCat.favorite = 'Box';
      console.log(childCat.favorite); // Box
      
      1
      2
      delete childCat.favorite; // true
      console.log(childCat.favorite); // undefined
      
      1
      2
    • 오브젝트의 proto 에 값 추가, 변경 가능 [[Prototype]] 에 새 프로퍼티를 추가합니다
      childCat.__proto__.gender = 'male';
      console.log(Object.getPrototypeOf(childCat)); // { name: 'DoonDoon', age: 10, gender: 'male' }
      
      1
      2

# 3. 상위 [[Prototype]] 체인에서 프로퍼티가 발견된 경우 🔗

  1. writable: true
    • 직속 프로퍼티를 생성하고 값을 할당
      • 의도는 상위 수준의 프로퍼티에 할당을 하려고 했지만 직속 프로퍼티가 추가 된다.
      • 객체지향에서 Overriding 이라고 부르는 Shadowing 이 발생한다.
    let anotherObj = {
        a: 2,
    };
    
    let myObject = Object.create(anotherObj);
    
    console.log(anotherObj.a);
    console.log(myObject.a);
    
    console.log(anotherObj.hasOwnProperty('a'));  //ture 
    console.log(myObject.hasOwnProperty('a'));  // false
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    이것이 위임을 통한 프로퍼티 가려짐이다. anotherObj.a가 증가함이 아니라. myObject에 a가 새로 할당된다.
    console.log(myObject.a++); // 2;  
    
    console.log(anotherObj.a);  //2 ;
    console.log(myObject.a); // 3;
    
    console.log(myObject.hasOwnProperty('a'));  // true
    
    1
    2
    3
    4
    5
    6
  2. writable: false
    • 아무일도 일어나지 않는다.
    • 엄격모드일 경우 TypeError

프로퍼티가 [Setter] 일 경우, 항상 Setter 가 호출된다.

  • 직속 프로퍼티가 추가 되지 않는다.
  • Setter 를 덮어 쓰려면 Object.defineProperty 를 사용해야 한다.

# getter 와 setter 똑똑하게 활용하기

# 1. 실제 프로퍼티 값을 감싸는 wrapper 처럼 사용

프로퍼티 값을 원하는 대로 통제 가능

let user = {
    get name () {
        return this._name;
    },
    set name (value) {
        if (value.length < 4) {
            alert('입력값이 너무 짧습니다. 4자 이상으로 입력하세요.');
            return;
        }
        this._name = value;
    }   
};
user.name = "Pete";
alert(user.name);

user.name = "";
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

user._name 으로 접근할 수 있지만. _ 밑줄로 시작하는 프로퍼티는 객체 내부에서만 활용하고, 외부에서는 건드리지 않는 것이 관습.

# 2. 호환성을 위해 사용하기

데이터 프로퍼티 name, age 를 사용해서 사용자를 나타내는 객체 구현

function User(name, age) {
    this.name = name;
    this.age = age;
}
let john = new User("John", 25);
alert(john.age);   // 25
1
2
3
4
5
6

요구사항이 바뀌어 age 를 birthday 로 저장해야 할 경우. (birthday 가 더 정확하고 편리하기 때문)

function User(name, birthday) {
    this.name = name;
    this.birthday = birthday;
}
let john = new User("John", new Date(1992, 6, 1));
1
2
3
4
5

기존 코드에 age 를 사용하고 있는 코드를 모두 찾아서 수정해야 하는 문제점이 있다.

function User(name, birthday) {
    this.name = name;
    this.birthday = birthday;
    Object.defineProperties(this, 'age', {
        get() {
            let todayYear = new Date().getFullYear();
            return todayYear - this.birthday.getFullYear();
        }
    });
}
let john = new User('John', new Date(1992, 6, 1));
alert(john.birthday);
alert(john.age);
1
2
3
4
5
6
7
8
9
10
11
12
13

# Property - Existence

객체에 프로퍼티가 존재하는지 여부를 확인하는 방법

const parent = { parentProp: 'Hey' };
const child = Object.create(parent);
child.childProp = 'Yay!';
1
2
3
  1. in 연산자
    • [[Prototype]] 체인을 모두 순회하며 키 존재 여부를 검사한다.
    console.log('parentProp' in child) // true
    
    1
  2. Object.hasOwnProperty
    • 직속 프로퍼티 존재 여부만을 검사한다.
    console.log(child.hasOwnProperty('parentProp')); // false
    console.log(child.hasOwnProperty('childProp')); // true
    
    1
    2

# Object.seal

  • 더 이상 확장 불가능 하게 만듦(프로퍼티 추가가 불가능한 상태)
'use strict';
const obj = { a: 1, b: 2 };
Object.seal(obj);

try {
  obj.c = 3;
} catch (e) {
// 확장 불가능 하기 때문에, TypeError!
  console.error(e); // TypeError: Cannot add property c, object is not extensible
}
1
2
3
4
5
6
7
8
9
10
  • Object.defineProperty 로 Descriptor 를 변경하는 행위를 막음
try {
    Object.defineProperty(obj, 'b', { enumerable: false });
  } catch (e) {
    console.error(e); // TypeError: Cannot redefine property: b
  }
1
2
3
4
5
  • 프로퍼티 삭제 불가
try {
  delete obj.a;
} catch (e) {
  console.error(e); // TypeError: Cannot delete property 'a' of #<Object>
}
1
2
3
4
5

# Object.freeze

  • Object.seal 의 모든 동작을 포함
  • 프로퍼티 값 또한 변경 불가능 하게 만든다.
  • 중첩된 접근 값이 변경은 가능하다.
    • obj.a = { b: 1 } 이라면 obj.a 는 변경 불가 하지만, obj.a.b 는 변경 가능합니다.